AWS WAFv2をCDK(Typescript)で構築してみた(2022年9月版)
みなさん、こんにちは。
AWS事業本部コンサルティング部の芦沢(@ashi_ssan)です。
CDKにこれまで全く触れていなかったのですが業務でCDKを扱う必要が出てきたため、最近CDKの勉強を始めました。
本エントリでは、以前作成したAWS WAFv2を作成するCloudFormationテンプレートをCDKで書いてみます。
なお、執筆者である私はインフラエンジニア出身でType Scriptをほとんど書いたことがありません。お手柔らかにコードを見ていただけると助かります。フィードバック大歓迎です。
同僚のたかくにから以下のAWS公式ブログの存在を教えてもらったことが本エントリを執筆するきっかけになりました。感謝です。
元ネタ
今回のCDKコードの元ネタになっているCloudFormationテンプレートはこちらのブログで紹介しているものです。
事前準備
事前にWAFに関連付けするALB等のリージョナルリソースが必要になります。
検証環境がない方はこちらのブログを参考すると、ALBが含まれるバックエンド環境をCDKで作成できるのでおすすめです。
作成できたら、作成したALBのARNをメモしておいてください。
CDKで書いてみた
今回利用するCDKのコードはこちらのGitHubリポジトリに上げています。
本エントリでは、lib/cdk_wafv2-stack.ts
について紹介します。
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; export class CdkWafv2Stack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // Define Variable const Prefix = 'devio'; const Env = 'develop'; const Scope = 'REGIONAL'; const WebAclAssociationResourceArn = 'arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/devio-stg-alb/xxxxxxxxxxxxxx'; // Define Resource Name const WebAclName = Env + '-' + Prefix + '-web-acl'; const S3ForWaflogName = 'aws-waf-logs-' + Env + '-' + Prefix + '-' + cdk.Stack.of(this).account; const S3ForAthenaQuery = 'athena-query-results-' + Env + '-' + Prefix + '-' + cdk.Stack.of(this).account; // S3 Config (Restricted Public Access) const public_access_block_config : cdk.aws_s3.CfnBucket.PublicAccessBlockConfigurationProperty = { blockPublicAcls: true, blockPublicPolicy: true, ignorePublicAcls: true, restrictPublicBuckets: true, }; // S3 const cfnS3ForWaflog = new cdk.aws_s3.CfnBucket(this, "S3BucketForWaflogConfig", { bucketName: S3ForWaflogName, publicAccessBlockConfiguration: public_access_block_config, }) const cfnS3ForAthenaQuery = new cdk.aws_s3.CfnBucket(this, "S3BucketForAthenaQueryConfig", { bucketName: S3ForAthenaQuery, publicAccessBlockConfiguration: public_access_block_config }) // WAF const cfnWebACL = new cdk.aws_wafv2.CfnWebACL(this,WebAclName,{ defaultAction: { allow: {} }, scope: Scope, visibilityConfig: { cloudWatchMetricsEnabled: true, metricName: WebAclName, sampledRequestsEnabled: true, }, name: WebAclName, rules: [ { name: 'AWS-AWSManagedRulesCommonRuleSet', priority: 0, statement: { managedRuleGroupStatement: { name:'AWSManagedRulesCommonRuleSet', vendorName:'AWS', excludedRules: [ {name: 'SizeRestrictions_BODY'}, {name: 'NoUserAgent_HEADER'}, {name: 'UserAgent_BadBots_HEADER'}, {name: 'SizeRestrictions_QUERYSTRING'}, {name: 'SizeRestrictions_Cookie_HEADER'}, {name: 'SizeRestrictions_BODY'}, {name: 'SizeRestrictions_URIPATH'}, {name: 'EC2MetaDataSSRF_BODY'}, {name: 'EC2MetaDataSSRF_COOKIE'}, {name: 'EC2MetaDataSSRF_URIPATH'}, {name: 'EC2MetaDataSSRF_QUERYARGUMENTS'}, {name: 'GenericLFI_QUERYARGUMENTS'}, {name: 'GenericLFI_URIPATH'}, {name: 'GenericLFI_BODY'}, {name: 'RestrictedExtensions_URIPATH'}, {name: 'RestrictedExtensions_QUERYARGUMENTS'}, {name: 'GenericRFI_QUERYARGUMENTS'}, {name: 'GenericRFI_BODY'}, {name: 'GenericRFI_URIPATH'}, {name: 'CrossSiteScripting_COOKIE'}, {name: 'CrossSiteScripting_QUERYARGUMENTS'}, {name: 'CrossSiteScripting_BODY'}, {name: 'CrossSiteScripting_URIPATH'} ] } }, visibilityConfig: { cloudWatchMetricsEnabled: true, metricName:'AWS-AWSManagedRulesCommonRuleSet', sampledRequestsEnabled: true, }, overrideAction: { none: {} }, } ] }); // Resource Associations const webAclAssociation = new cdk.aws_wafv2.CfnWebACLAssociation(this,"webAclAssociation", { resourceArn: WebAclAssociationResourceArn, webAclArn: cfnWebACL.attrArn, } ) webAclAssociation.addDependsOn(cfnWebACL) // Export WAF logs to S3 const cfnLoggingConfiguration = new cdk.aws_wafv2.CfnLoggingConfiguration(this, 'CfnLoggingConfiguration', { logDestinationConfigs: [cfnS3ForWaflog.attrArn], resourceArn: cfnWebACL.attrArn, }); } }
各箇所について、それぞれ説明していきます。
こちらがCloudFormationテンプレートのParameters
の箇所にあたるところです。わかりやすいように変数として指定してみました。
WebAclAssociationResourceArn
の変数内のARNはダミーを入力しています。実際にCDKを利用する際は検証環境のALB ARNを入力してください。
// Define Variable const Prefix = 'devio'; const Env = 'develop'; const Scope = 'REGIONAL'; const WebAclAssociationResourceArn = 'arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/devio-stg-alb/xxxxxxxxxxxxxx';
ここでS3の設定を行なっています。
WAFログ保存用のバケットとAthenaクエリ保存用のバケット1を作成します。
どちらもパブリックアクセスは不要なため、public_access_block_config
でパブリックアクセスをすべて禁止する設定にしました。
// S3 Config (Restricted Public Access) const public_access_block_config : cdk.aws_s3.CfnBucket.PublicAccessBlockConfigurationProperty = { blockPublicAcls: true, blockPublicPolicy: true, ignorePublicAcls: true, restrictPublicBuckets: true, }; // S3 const cfnS3ForWaflog = new cdk.aws_s3.CfnBucket(this, "S3BucketForWaflogConfig", { bucketName: S3ForWaflogName, publicAccessBlockConfiguration: public_access_block_config, }) const cfnS3ForAthenaQuery = new cdk.aws_s3.CfnBucket(this, "S3BucketForAthenaQueryConfig", { bucketName: S3ForAthenaQuery, publicAccessBlockConfiguration: public_access_block_config })
ここが今回のメインのAWS WAFのWeb ACLに関する箇所です。
基本的にはCloudFormationテンプレートと同じ感覚で書くことができました。
excludedRules
の箇所についてはサブルール1つごとに{}で閉じる必要があるため注意です(私はそこでハマってしまいました)
// WAF const cfnWebACL = new cdk.aws_wafv2.CfnWebACL(this,WebAclName,{ defaultAction: { allow: {} }, scope: Scope, visibilityConfig: { cloudWatchMetricsEnabled: true, metricName: WebAclName, sampledRequestsEnabled: true, }, name: WebAclName, rules: [ { name: 'AWS-AWSManagedRulesCommonRuleSet', priority: 0, statement: { managedRuleGroupStatement: { name:'AWSManagedRulesCommonRuleSet', vendorName:'AWS', excludedRules: [ {name: 'SizeRestrictions_BODY'}, {name: 'NoUserAgent_HEADER'}, {name: 'UserAgent_BadBots_HEADER'}, {name: 'SizeRestrictions_QUERYSTRING'}, {name: 'SizeRestrictions_Cookie_HEADER'}, {name: 'SizeRestrictions_BODY'}, {name: 'SizeRestrictions_URIPATH'}, {name: 'EC2MetaDataSSRF_BODY'}, {name: 'EC2MetaDataSSRF_COOKIE'}, {name: 'EC2MetaDataSSRF_URIPATH'}, {name: 'EC2MetaDataSSRF_QUERYARGUMENTS'}, {name: 'GenericLFI_QUERYARGUMENTS'}, {name: 'GenericLFI_URIPATH'}, {name: 'GenericLFI_BODY'}, {name: 'RestrictedExtensions_URIPATH'}, {name: 'RestrictedExtensions_QUERYARGUMENTS'}, {name: 'GenericRFI_QUERYARGUMENTS'}, {name: 'GenericRFI_BODY'}, {name: 'GenericRFI_URIPATH'}, {name: 'CrossSiteScripting_COOKIE'}, {name: 'CrossSiteScripting_QUERYARGUMENTS'}, {name: 'CrossSiteScripting_BODY'}, {name: 'CrossSiteScripting_URIPATH'} ] } }, visibilityConfig: { cloudWatchMetricsEnabled: true, metricName:'AWS-AWSManagedRulesCommonRuleSet', sampledRequestsEnabled: true, }, overrideAction: { none: {} }, } ] });
最後にログ出力設定とWAFへのリソース関連付け設定の箇所についてです。
こちらもCloudFormationと同じ感覚で書いていました。
組み込み関数を利用する必要がないので、コードがシンプルだと感じます。
// Resource Associations const webAclAssociation = new cdk.aws_wafv2.CfnWebACLAssociation(this,"webAclAssociation", { resourceArn: WebAclAssociationResourceArn, webAclArn: cfnWebACL.attrArn, } ) webAclAssociation.addDependsOn(cfnWebACL) // Export WAF logs to S3 const cfnLoggingConfiguration = new cdk.aws_wafv2.CfnLoggingConfiguration(this, 'CfnLoggingConfiguration', { logDestinationConfigs: [cfnS3ForWaflog.attrArn], resourceArn: cfnWebACL.attrArn, });
説明は以上です。
CDKでデプロイ/削除してみる
こちらのGitHubリポジトリに今回のCDKコードをアップロードしています再掲します。Cloneしてご利用ください。
git clone https://github.com/h-ashisan/aws-cdk-create-wafv2-2022-9.git
Cloneしたディレクトリ配下に移動して、パッケージをインストールします
npm install
こちらのコマンドでデプロイしてください
cdk deploy
正常終了すると、マネジメントコンソール側(CloudFormation、WAF)でも正常にリソースが作成できていることが確認できます。
削除する際には、WAFログ用のS3バケット(aws-waf-logs-develop-devio-123456789012
)の中身を事前に空にする必要がありますので注意です。
その後、以下コマンドで削除できます。
cdk destroy
最後に
ガッツリとCDKに触れたのは今回が初めてでしたが、L1の抽象度で書いたおかげなのかCloudFormartionテンプレートと同じ感覚でコード書くことができた印象が強いです。
CDKを利用することによって、CloudFormationテンプレートにはなかった「コードのシンプルさ」「デプロイ/削除の簡単さ」が体験できました。
今後もCDKでいろんなサービスを構築してみたいと思いました。
以上、AWS事業本部コンサルティング部の芦沢(@ashi_ssan)でした。
参考
- Easily protect your AWS CDK-defined infrastructure with AWS WAFv2 | AWS DevOps Blog
- AWS WAFv2をCDKで構築してみた | DevelopersIO
- [AWS CDK] APIGateway+WAFv2 Web ACL構成でデプロイしようとしてちょっとハマった点
- @aws-cdk/aws-wafv2 module · AWS CDK
- 実践!AWS CDK #2 VPC | DevelopersIO
- 実践!AWS CDK #18 ALB | DevelopersIO
- 元ネタのCloudFormationテンプレートでは、後の手順でAthenaテーブルを作成して分析を行うため、Athena用のバケットも作成しています ↩